/*服务端业务处理模块：将网络通信模块和业务处理进行了合并（网络通信通过httplib库完成）
    1.搭建网络通信服务器：借助httplib完成
    2.业务处理请求
        1）文件上传请求：备份客户端上传的文件，响应上传成功
        2）文件列表请求：客户端浏览器请求一个备份文件的展示页面，响应页面
        3）文件下载请求：通过展示页面，点击下载，响应客户端要下载的文件数据


    网络通信接口设计：约定好，客户端发送什么样的请求，我们给予什么样的响应
        请求：文件上传、展示页面、文件下载

        上传：
            请求：
            POST /upload HTTP/1.1
            当服务器收到了一个POST方法的/upload请求，我们则认为这是一个文件上传请求，解析请求，得到文件数据，将数据写入到文件中

            响应：
            HTTP/1.1 200 OK
            .......

        展示：
            请求：
            GET /listshow HTTP/1.1

            响应：
            HTTP/1.1 200 OK
            Content-Length : .........
            ........

        下载：
            请求：
            GET /download/text.txt HTTP/1.1

            响应：
            HTTP/1.1 200 OK
            Content-Length : .........
            正文就是文件数据
*/

#pragma once
#include <string>

#include "httplib.h"
#include "data.hpp"

extern cloud::DataManager *_data;
namespace cloud
{
    // 搭建http服务器，并进行业务处理
    class Service
    {
    private:
        int _server_port;
        std::string _server_ip;
        std::string _download_prefix;
        httplib::Server _server;

    public:
        Service()
        {
            Config *config = Config::GetInstance();
            _server_port = config->GetServerPort();
            _server_ip = config->GetServerIp();
            _download_prefix = config->GetDownloadPrefix();
        }

        bool RunModule()
        {
            _server.Post("/upload", Upload);

            // 不把ListShow设置为静态函数，这样写为什么这样不行
            //  _server.Get("/listshow", std::bind(&cloud::Service::ListShow, this, std::placeholders::_1, std::placeholders::_2));
            //  _server.Get("/", std::bind(&cloud::Service::ListShow, this, std::placeholders::_1, std::placeholders::_2));

            _server.Get("/listshow", ListShow);
            _server.Get("/", ListShow);
            std::string download_url = _download_prefix + "(.*)"; // 正则表达式，匹配任意一个字符任意次
            _server.Get(download_url, Download);
            _server.listen("172.24.35.90", _server_port); // 匹配服务器的"172.24.35.90"ip（这是私网ip，也就是本服务器主机自己的网卡，不能用公网ip，那不是本主机的网卡，匹配不到）

            return true;
        }

    private:
        static void Upload(const httplib::Request &req, httplib::Response &rsp) // 上传
        {
            // Post /ipload 文件数据在正文中（正文中不全不都是文件数据）
            auto ret = req.has_file("file"); // 判断有没有上传的文件区域
            if (ret == false)
            {
                rsp.status = 400;
                return;
            }
            const auto &file = req.get_file_value("file");
            // file.filename->文件名称，file.content->文件数据
            std::string back_dir = Config::GetInstance()->GetBackDir();
            std::string realpath = back_dir + FileUtil(file.filename).FileName();
            FileUtil fu(realpath);
            fu.SetContent(file.content); // 将数据写入文件中
            BackupInfo info;
            info.NewBackupInfo(realpath); // 组织备份的文件信息
            _data->Insert(info);          // 向数据管理模块添加备份的文件信息
            return;
        }

        static std::string TimeToStr(time_t t)
        {
            std::string tmp = std::ctime(&t);
            return tmp;
        }

        static void ListShow(const httplib::Request &req, httplib::Response &rsp) // 页面展示
        {
            // 1.获取所有的文件备份信息
            std::vector<BackupInfo> arry;
            _data->GetAll(&arry);

            // 2.根据所有备份信息，组织html文件数据
            std::stringstream ss;
            ss << "<head><title>Download</title></head>";
            ss << "<body><h1>Download</h1><table>";
            for (auto &a : arry)
            {
                ss << "<tr>";
                std::string filename = FileUtil(a.real_path).FileName();
                ss << "<td><a href='" << a.url << "'>" << filename << "</a></td>";
                ss << "<td align='right'>" << TimeToStr(a.mtime) << "</td>";
                ss << "<td align='right'>" << a.fsize / 1024 << "k</td>";
                ss << "</tr>";
            }
            ss << "</table></body></html>";
            rsp.body = ss.str();
            rsp.set_header("Content-Type", "text/html");
            rsp.status = 200;
        }

        /*http的ETag头部字段：其中存储了一个资源的唯一标识
            客户端第一次下载文件的时候，会收到这个响应信息
            第二次下载，就会将这个信息发送给服务器，想要让服务器根据这个唯一标识判断这个资源有没有被修改过
            如果没有被修改过，直接使用原先缓存的数据，不用重新下载了

            http协议本身对于etage中是什么数据并不关心，只要服务端能够自己标识就行
            因此我们etag就用 "文件名-文件大小-最后一次修改时间" 组成
            etage再断点续传也要用到。


            http协议的Accept-Ranges：bytes 字段：用于告诉客户端，本服务器支持断点续传，并且数据单位以字节为单位

        */
        static std::string GetETag(const BackupInfo &info)
        {
            // etg:filename-fsize-mtime
            FileUtil fu(info.real_path);
            std::string etag = fu.FileName();
            etag += "-";
            etag += std::to_string(info.fsize);
            etag += "-";
            etag += std::to_string(info.mtime);

            return etag;
        }


        static void Download(const httplib::Request &req, httplib::Response &rsp) // 下载
        {
            // 1.获取客户端请求的资源路径path
            // 2.根据资源路径，获取文件备份信息
            BackupInfo info;
            _data->GetOneByUrl(req.path, &info);

            // 3.判断文件是否被压缩，如果被压缩，要先解压缩，并删除压缩包，修改备份信息
            if (info.pack_flag)
            {
                FileUtil fu(info.pack_path);
                fu.UnCompress(info.real_path);
                fu.Remove();
                info.pack_flag = false;
                _data->Updata(info);
            }

            // 4.读取文件数据，放入rsp.body中
            FileUtil fu(info.real_path);

            bool retrans = false; // 用于标记是否是断点续传
            // 判断是不是断点续传
            std::string old_etag;
            if (req.has_header("If-Range"))
            {
                old_etag = req.get_header_value("If-Range");
                if (old_etag == GetETag(info))
                {
                    // 如果有If-Range字段，且它的值与请求文件的最新etag一样，则是断点续传
                    retrans = true;
                }
            }

            if (retrans == false)
            {
                // 如果没有If-Range字段，则是正常下载，或者如果有这个字段，但是它的值与当前文件的etag不一样
                // 也认为是正常下载

                fu.GetContent(&rsp.body);
                // 5.设置响应头部字段：ETag，Accept-Ranges:bytes
                rsp.set_header("Accept-Ranges", "bytes");
                rsp.set_header("ETag", GetETag(info));
                rsp.set_header("Content-Type", "application/octet-stream"); // 表明响应的数据是一个二进制数据流，常用于文件下载
                rsp.status = 200;
            }
            else
            {
                // 断点续传
                fu.GetContent(&rsp.body);
                // httplib中对断点续传做了处理，对rsp的body部分进行了substr操作，截取了区间内的数据
                // 所以我们只需要把文件里的所有数据读取到response的body里面就可以了
                // httplib会去用substr截取指定范围内的数据

                // std::string range = req.get_header_val("Range");
                // bytes = start - end

                rsp.set_header("Accept-Ranges", "bytes");
                rsp.set_header("ETag", GetETag(info));
                rsp.set_header("Content-Type", "application/octet-stream");
                // 在httplib中，响应报头"Content-Range"也已经设置好了
                //  rsp.set_header("Content-Range", "bytes start - end/fsize");

                rsp.status = 206; //区间请求响应的是206
            }
        }

        /*断点续传
            功能：当文件下载过程中，因为某种异常而中断，如果再次进行从头下载，效率太低，因此断点续传就是从上次下载断开的位置下载即可
            目的：提高文件传输效率
            实现思想：
                客户端在下载文件的时候，需要每次在接受到文件后，记录当前下载的数据量。
                当异常下载中断时，下次断点续传的时候，将要重新下载的数据区间（下载起始位置，结束位置）
                发送给服务器，服务器收到后，仅仅回传客户端需要的区间的数据即可。
            需要考虑的问题：
                如果上次下载文件后，这个文件在服务器上已经被修改了，这时候不能断点续传，而应该将整个文件重新下载

            http协议中，断点续传的实现：
                主要关键点在于能够告诉服务器下载区间返回，以及服务器上要能检测上一次下载之后，这个文件是否修改过

            文件下载：
                GET /download/test.txt HTTP/1.1
                ...

                HTTP/1.1 200 OK
                Accept-Ranges：bytes 告诉客户端，服务器支持断点续传功能
                ETag："asdgasefza-文件唯一标识" 客户端收到响应会保存这个信息，后面会放到二次下载请求的If_Range中
                ..
                文件数据作为正文

            断点续传：
                GET /download/test.txt HTTP/1.1
                If_Range:"服务端在下载时响应的etag字段"，用于服务器判断这个文件与原先下载的文件是否一致
                Range:bytes 100-100000：区间范围，用于告诉服务器，客户端需要的数据区间范围

                HTTP/1.1 206 部分内容，服务器成功处理了部分请求
                ETag:"asdgasefza"
                Content-Ranges:bytes 100-10000/文件大小

                正文就是对应的区间数据

        */
    };
}
